바이트 순서·메시지 헤더·컴파일러 경고 실습 — htonll·enc_dec와 size_t·switch 함정

NOTE

C 기초 강의의 기타 실습(16-01 ~ 16-05, “컴파일러가 경고를 주는 경우”)에서 만난 오류 패턴을 정리하고, 이어서 64비트 바이트 순서 변환 함수(byteorder.c)와 16바이트 메시지 헤더 encode/decode 모듈(enc_dec.h·enc_dec.c·enc_dec_test.c), TLV 파서 코드 리뷰(tag_length_value.c)를 다룬다.

환경은 Docker Desktop의 Linux 컨테이너(c_app_part1), gcc -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다. 컨테이너는 x86이라 리틀엔디안이며, 빅엔디안 호스트에서의 동작은 실행해 보지 못했다 (엔디안에 의존하지 않도록 시프트로 구현했지만 확인한 것은 아니다).

TIP

📂 원본 소스 (GitHub)

  • 강의 코드 16-01 ~ 16-05, TLV 리뷰 과제: lecture/msc/ (tag_length_value.c는 컴파일되지 않는 상태라 make lecture에서 제외)
  • 64비트 바이트 순서: msc/byteorder.c
  • 헤더 encode/decode: msc/enc_dec.h · msc/enc_dec.c · 테스트 msc/enc_dec_test.c
  • 빌드/실행: make -C c_basic 후 ./c_basic/build/msc/enc_dec_test (README)
  • 컨테이너 원본 경로는 ~/coding/16_msc/ (접속: docker exec -it c_app_part1 /bin/bash)

📌 한눈에 보기

실습문제처방
16-01size_t로 enum을 순회, switch에 default가 없어 범위 밖 값에서 return 누락순회는 enum 타입(또는 int)으로, switch는 default에서 에러 처리
16-02도달할 수 없는 if (s) (dead code)죽은 분기를 지우고 흐름을 단순화
16-03p2 대입이 쓰이지 않음, baz()가 항상 같은 값쓰이지 않는 값·상수 함수는 의도를 의심
16-04size_t i <= SIZE_MAX가 항상 참 + i += step 랩어라운드 → 무한 루프증가시키기 전에 남은 범위를 검사
16-05sizeof(배열)(바이트 수)을 원소 수로 넘김, 반복문 뒤 return 없음sizeof(a)/sizeof(a[0]), 모든 경로에 return
byteorder마스크 &보다 시프트가 먼저, 시프트 양 오류, 결과를 버림(n & 마스크) << k + 바이트별 이동량 표로 검산
enc_dec구조체 통째 memcpy는 엔디안·패딩이 새어 나옴필드 단위 시프트 변환, 검증 후에만 출력
TLV종료 조건·반환·부호 없는 비교·타입 오타 등코드 리뷰 체크리스트 (§4)

1. 컴파일러 경고가 알려주는 결함 — 16-01 ~ 16-05

컴파일러가 warning을 주는 경우. 경고 자체를 “없애는” 것이 아니라, 경고가 가리키는 설계 결함을 고치는 것이 목적이다.

16-00. 경고는 가장 높은 수준으로 켜고, 남기려면 이유를 쓴다

요약

  • 문제: 경고를 끄거나 무시하고, 최적화가 의도한 코드를 지움
  • 원인: -Wall은 모든 경고가 아니며, 읽는 곳이 없는 memset 같은 dead store는 -O2에서 제거됨
  • 처방: -Wall -Wextra(필요하면 -Wswitch-enum 등)로 컴파일하고 남기는 경고는 이유를 주석으로. 민감값은 explicit_bzero·memset_s·volatile로 지움
  • 결과: gcc 4.8.5: -O0 호출 1개, -O2 0개

16-01. size_t로 enum 순회 + default 없는 switch

요약

  • 문제: size_t로 enum을 순회하고, default 없는 switch가 범위 밖 값에서 return 없이 끝남
  • 원인: 범위 밖 enum 값 처리 누락, 부호 있는 enum을 size_t로 암묵 변환
  • 처방: 순회는 enum/int와 COLOR_MAX, switch는 모든 case + default(미정의 값용). 누락 탐지는 -Wswitch-enum
  • 결과: default를 넣으면 -Wall이 빠진 case를 더는 알려 주지 않음

16-02. dead code

요약

  • 문제: 항상 거짓인 if (s) 분기 (dead code)
  • 원인: 앞에서 return해서 이 경로의 s는 초기값 NULL
  • 처방: 죽은 분기를 지우고 흐름을 단순화. a == b;, *p++;처럼 효과 없는 코드는 의도 확인

16-03. 쓰이지 않는 값과 상수 함수

요약

  • 문제: 쓰이지 않는 대입(p2)과 항상 같은 값을 주는 baz()
  • 원인: static 변수가 어디서도 바뀌지 않아 if (baz())는 항상 거짓, return p1;은 도달 불가
  • 처방: 쓰이지 않는 값은 로직 오류의 신호이므로 찾아서 정리. 버리는 반환값은 (void) 캐스트로 의도를 표시

16-04. 부호 없는 정수의 랩어라운드와 항상 참인 조건

요약

  • 문제: size_t i <= SIZE_MAX가 항상 참이고 i += step 랩어라운드로 무한 루프
  • 원인: 부호 없는 정수는 최댓값을 넘으면 0쪽으로 돌아옴
  • 처방: 증가시키기 전에 SIZE_MAX - i < step 검사. 종료 조건은 !=보다 </<=
  • 결과: %zu로 출력해야 값이 맞게 보임

16-05. sizeof는 원소 수가 아니라 바이트 수

요약

  • 문제: sizeof(배열)(바이트 수)을 원소 수로 넘기고, 반복문 뒤 return 누락
  • 원인: delim이 없으면 배열 뒤 메모리를 읽고, 반환값이 쓰레기라 data[i]가 범위 밖 쓰기
  • 처방: sizeof(a)/sizeof(a[0]), 모든 경로에 return(못 찾음은 에러 코드 + *out), 반환값 검사 후에만 접근
  • 결과: delim=2처럼 찾으면 정상처럼 보여서 경고 없이는 놓침

2. 바이트 순서 — byteorder.c

2-1. 개념

2-2. 64비트 구현과 원본의 실수

요약

  • 문제: 64비트 htonll/ntohll 첫 구현의 6가지 실수
  • 원인: <stdint.h> 누락, 결과 버림, 시프트가 &보다 먼저, 시프트 양 오류(40을 32로), %x, 반복 패턴 테스트 값
  • 처방: (n & 마스크) << k와 이동량 표 (7−2i)×8로 검산, PRIx64, 0x0102030405060708처럼 바이트마다 다른 값

2-3. 검증

요약

  • 처방: 메모리 바이트 확인 + 왕복 + __builtin_bswap64 비교 + 경계 값 7개
  • 결과: 7개 모두 PASS, 시프트 두 곳을 32로 바꾼 변이는 7개 중 4개만 통과 (검증이 버그를 잡음)

3. 메시지 헤더 encode / decode — enc_dec

3-1. 요구사항과 와이어 포맷

flowchart LR
    subgraph HDR["헤더 16바이트 (모든 다바이트 필드는 빅엔디안)"]
        direction LR
        A["0: version<br/>1바이트"] --- B["1: type<br/>1바이트"] --- C["2~3: length<br/>2바이트"] --- D["4~7: seq<br/>4바이트"] --- E["8~15: session_id<br/>8바이트"]
    end

3-2. 설계 결정

요약

  • 문제: 헤더 16바이트를 network byte order로 encode/decode
  • 원인: 구조체 memcpy는 엔디안·패딩이 새어 나옴
  • 처방: 필드 단위 시프트 변환(put_be*/get_be*), 에러는 원인별 음수, 검증 후에만 출력(실패 시 출력 불변), decode도 version/type 검증

3-3. 테스트 프로그램 (34개 검사)

요약

  • 처방: byte 순서·왕복·에러·msg_type_str 4구분 34개 검사 + 변이 테스트
  • 결과: 34개 모두 통과, 일부러 넣은 버그 6종이 모두 실패로 잡힘

4. 코드 리뷰 — TLV 파서 (tag_length_value.c)


5. 정리 — 체크리스트

정수·경계

  • size_t(부호 없음) 뺄셈은 빼기 전에 대소 비교 (len < HDR_LEN), < 0 비교는 항상 거짓
  • 증가시키는 루프는 증가 전에 오버플로(랩어라운드)를 검사하고, i <= SIZE_MAX 같은 항상 참인 조건을 쓰지 않는다
  • sizeof(배열)은 바이트 수. 원소 수는 sizeof(a)/sizeof(a[0])
  • 반복문 뒤나 switch 뒤에도 모든 경로가 return 하는가 (-Wreturn-type)
  • size_t는 %zu, 64비트는 PRIx64

switch·enum

  • enum을 switch할 때 모든 case를 나열하고, default는 “정의되지 않은 값” 처리용으로만 둔다
  • -Wswitch-enum을 켜서 default에 가려진 누락을 잡는다
  • 외부에서 온 값을 enum으로 캐스팅하기 전에 범위를 검사한다

바이트 순서·직렬화

  • 네트워크로 보내는 정수는 필드 단위로 변환한다 (구조체 통째 memcpy 금지: 엔디안·패딩)
  • 시프트 변환(put_be*/get_be*)은 정렬에 무관하고, get은 시프트 전에 넓은 타입으로 캐스팅
  • (n & 마스크) << k — 시프트가 &보다 먼저이므로 괄호
  • 외부에서 받은 헤더는 decode 후 version·type·길이를 검증하고, 실패하면 출력 구조체를 건드리지 않는다
  • 에러는 원인별 음수로 구분한다

코드 상태

  • 도달할 수 없는 코드(dead code)와 쓰이지 않는 대입이 없는가
  • 빌드는 -Wall -Wextra, 새 경고는 코드로 해결 (억제하지 않는다)
  • 변이(일부러 넣은 버그)가 코드를 실제로 바꿨고, 테스트가 그것을 잡는지 확인했는가

관련 노트